package com.sunday.common.mq.rabbit.study.spring.study;

import com.google.gson.Gson;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.support.converter.*;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

/**
 * https://docs.spring.io/spring-amqp/docs/current/reference/html/#spring-rabbit-json
 *
 * 4.4.3. 从非 Spring 应用程序接收 JSON
 * Spring 应用程序在发送 JSON 时，将标头设置为完全限定的类名，以帮助接收应用程序将 JSON 转换回 Java 对象。TypeId
 * 该示例探讨了从非 Spring 应用程序转换 JSON 的几种技术。spring-rabbit-json
 * 另请参阅Jackson2JsonMessageConverter以及DefaultClassMapper的Javadoc。
 *
 * org.springframework.amqp.support.converter.SmartMessageConverter 是 Spring AMQP 提供的消息转换器接口，用于在应用程序和消息代理之间进行消息转换。实现该接口的类可以根据目标对象类型、消息头和消息体的内容来自动选择合适的消息转换策略，从而将消息转换为目标对象或将目标对象转换为消息。它的作用是简化消息传递过程中的数据格式转换，提高应用程序的开发效率和可维护性。
 * org.springframework.beans.factory.BeanClassLoaderAware 是一个接口，用于为实现它的类提供一个回调方法 setBeanClassLoader(ClassLoader classLoader)，该方法允许在创建 bean 实例时将其所在的 ClassLoader 设置为 bean 实例的 ClassLoader。这对于需要动态加载类或资源的情况非常有用，例如在使用反射或自定义类加载器时。
 */
@Slf4j
public class GsonJsonMessageConverter extends AbstractMessageConverter implements BeanClassLoaderAware, SmartMessageConverter {

    private final Gson gson = new Gson();

    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    private Charset defaultCharset = DEFAULT_CHARSET;

    private MimeType supportedContentType = MimeTypeUtils.parseMimeType(MessageProperties.CONTENT_TYPE_JSON);

    private ClassLoader classLoader = ClassUtils.getDefaultClassLoader();

    private String supportedCTCharset = this.supportedContentType.getParameter("charset");

    private ClassMapper classMapper = new DefaultClassMapper();

    @Setter
    private boolean assumeSupportedContentType = true;

    public GsonJsonMessageConverter() {
        this("*");
    }

    public GsonJsonMessageConverter(String... trustedPackages) {
        ((DefaultClassMapper) this.classMapper).setTrustedPackages(trustedPackages);
    }

    public void setDefaultCharset(@Nullable String defaultCharset) {
        this.defaultCharset = (defaultCharset != null) ? Charset.forName(defaultCharset) : DEFAULT_CHARSET;
    }

    public String getDefaultCharset() {
        return this.defaultCharset.name();
    }

    @Nullable
    public ClassMapper getClassMapper() {
        return this.classMapper;
    }

    public void setSupportedContentType(MimeType supportedContentType) {
        Assert.notNull(supportedContentType, "'supportedContentType' cannot be null");
        this.supportedContentType = supportedContentType;
        this.supportedCTCharset = this.supportedContentType.getParameter("charset");
    }

    @Override
    protected Message createMessage(Object object, MessageProperties messageProperties) {

        byte[] bytes;
        try {
            bytes = gson.toJson(object).getBytes(defaultCharset);
        } catch (Exception e) {
            throw new MessageConversionException("Failed to convert Message content", e);
        }

        messageProperties.setContentType(this.supportedContentType.toString());
        messageProperties.setContentEncoding(getDefaultCharset());
        messageProperties.setContentLength(bytes.length);

        getClassMapper().fromClass(object.getClass(), messageProperties);
        return new Message(bytes, messageProperties);
    }

    @Override
    public Object fromMessage(Message message) throws MessageConversionException {
        return fromMessage(message, null);
    }

    @Override
    public Object fromMessage(Message message, @Nullable Object conversionHint) throws MessageConversionException {
        Object content = null;
        MessageProperties properties = message.getMessageProperties();
        if (properties != null) {
            String contentType = properties.getContentType();
            if ((this.assumeSupportedContentType && (contentType == null || contentType.equals(MessageProperties.DEFAULT_CONTENT_TYPE))) || (contentType != null && contentType.contains(this.supportedContentType.getSubtype()))) {
                String encoding = determineEncoding(properties, contentType);

                Class<?> targetClass = getClassMapper().toClass(message.getMessageProperties());
                content = convertBytesToObject(message.getBody(), encoding, targetClass);

            } else {
                if (this.log.isWarnEnabled()) {
                    this.log.warn("Could not convert incoming message with content-type [" + contentType + "], '" + this.supportedContentType.getSubtype() + "' keyword missing.");
                }
            }
        }
        if (content == null) {
            content = message.getBody();
        }
        return content;
    }

    private Object convertBytesToObject(byte[] body, String encoding, Class<?> targetClass) {
        Object content;
        try {
            content = this.gson.fromJson(new String(body, encoding), targetClass);
        } catch (IOException e) {
            throw new MessageConversionException("Failed to convert Message content", e);
        }
        return content;
    }


    private String determineEncoding(MessageProperties properties, @Nullable String contentType) {
        String encoding = properties.getContentEncoding();
        if (encoding == null && contentType != null) {
            try {
                MimeType mimeType = MimeTypeUtils.parseMimeType(contentType);
                if (mimeType != null) {
                    encoding = mimeType.getParameter("charset");
                }
            } catch (RuntimeException e) {
            }
        }
        if (encoding == null) {
            encoding = this.supportedCTCharset != null ? this.supportedCTCharset : getDefaultCharset();
        }
        return encoding;
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }
}
